# -*- coding: utf-8 -*-


class PortIn:
    """ PortIn class abstracts the input port as a dual-virtual FIFO 
    queues. Those queues are transparent from user. The next and 
    current queues alternate to simulate FIFO write delays and 
    guarantee correctness. Note the physical implementation still 
    corresponds to a single FIFO queue. However, the user should not 
    let the logic code rely on the future values.
    
    The data type transferred inside the port are free to use. However,
    it is strongly suggested that only basic types should be used in 
    the port transferring. These types include
        - integer
        - float
        - double
        - boolean
    
    The depth of the PortIn queue will be updated when data put or get
    operation happens. None of the queues are protected. All the ports
    are NOT thread-safe. The depth of the PortIn queue could be either 
    unlimited or limited with the by calling the method 
    `set_max_size()`.
    
    Examples:
        To create a PortIn type manually inside a Node, for example, 
        create a PortIn type named as `port_write_enable`::
        
            self.port_in('port_write_enable')
            
        To get a data from the port, using the `get()` method from the 
        port, the first value from the queue. If there is no value 
        holds in the port, an Exception will raise.
         
        For example, to get a value from PortIn variable 
        `port_write_enable`::
        
            port_value = port_write_enable.get()
        
        To set a data to the port, using the `put()` method of the port. It is a
            similar operation to the `get()` operation. For example, to put a value
            to the PortIn variable, `port_write_enable` of a `Node` instance `ram`::
                ram.port_write_enable.put(False)
    """

    def __init__(self, max_size: int = -1, almost_full_size: int = -1) -> 'T':
        # Create the queue buffer and current item holder.
        self.__buffer = []
        self.__current = None
        # Prepare variables.
        self.__max_usage = 0
        # Prepare constants.
        #  Maximum size.
        self.__max_size = -1
        self.set_max_size(max_size)
        self.__almost_full_size = -1
        self.set_almost_full_size(almost_full_size)

    def current(self) -> 'T':
        """ Read the last get value of the PortIn instance. This operation will
        not change the queue. The read of last get value is from another buffer.
        Returns: The last result of the `get()` operation. If `get()` operation
        has never been executed before, it will return `None`.
        """
        return self.__current

    def size(self) -> int:
        """ Get the current size of the queue.
        Returns: The number of the items held by current queue.
        """
        return len(self.__buffer)

    def max_size(self) -> int:
        """ Get the max size of the queue. For an unlimited port, the max size
        is -1.
        Returns: The max size of the items could be held in this port.
        """
        return self.__max_size

    def set_max_size(self, max_size: int) -> None:
        """ Set the maximum item number that the current queue could hold.
        Args:
            max_size: The number of the maximum holding items.
        """
        # Save the maximum size
        self.__max_size = max_size

    def set_almost_full_size(self, almost_full: int = -1) -> None:
        """ Set the almost full callback triggered threshold size. The threshold
        size should be less than the greatest index of the queue could hold and
        greater than 0.
        Please notice: the almost full callback will be only triggered when
        putting an item to the queue. If the threshold is reached from getting a
        value, the callback method will not be executed.
        If the maximum size is -1, i.e. unlimited size, this method does not
        work at all.
        Args:
            almost_full: The almost full callback triggered size threshold.
        """
        self.__almost_full_size = (self.__max_size - 2) \
            if almost_full == -1 and self.__max_size > 0 else almost_full

    def is_full(self) -> bool:
        """ To check whether the port is already full. If the PortIn has
        unlimited size, this function will always return `False`.
        Returns: If the port reaches the max size, returns `True`, otherwise,
        returns `False`.
        """
        return (self.__max_size > 0) and (self.size() >= self.__max_size)

    def is_empty(self) -> bool:
        """ To check whether the port is empty, which means there is no value
        holds in the port queue.
        Returns: If the port holds no value, returns `True`, otherwise, returns
        `False`.
        """
        # Connecting node reference this signal to determine get.
        return self.size() == 0

    def head(self) -> 'T':
        """ Get the first coming item in the queue.
        Returns: If the queue is not empty, it will show the first item
        contained in the port, or else raise an Exception.
        """
        # Check the size first.
        if self.size() == 0:
            raise Exception('No data is currently holding in port.')
        return self.__buffer[0]

    def put(self, value: 'T') -> None:
        """ Blocking put, will retry until consumed. Put into the next queue
        such that it is read for consumption in the
            next step. The value will be arrived at the next step, i.e. after
            the `next()` method is called.
        Args:
            value: The new value to be put in to the port.
        """
        self.__buffer.append(value)
        self.__max_usage = max(self.__max_usage, self.size())

    def get(self) -> 'T':
        """ Get one value from the port.
        Returns: The value at the head of the queue. If the queue is empty at
        the moment, raise an Exception.
        """
        # Check the size first.
        if self.size() == 0:
            raise Exception('No data is currently holding in port.')
        # Save the queue data into current.
        self.__current = self.__buffer.pop(0)
        # Give the current size back.
        return self.__current

    def max_history_usage(self) -> int:
        """ Get the maximum history usage statistic result.
        Returns: The maximum usage level of the current port.
        """
        return self.__max_usage

    def current_usage(self) -> int:
        """ Get the current usage of the port.
        Returns: The current usage level of the port queue.
        """
        return self.size()


class PortInDirect(PortIn):
    """ PortInDirect simulates the direct assign port or a cache-less queue. It
    only holds one value, which is the last set value. The `put()` and `get()`
    value will only operate the only value. Works just like a register holding
    the value.
    For PortInDirect, it does not support any callback that a normal PortIn has.
    Both `is_all_full` and `is_all_empty` always returns `False`. The history
    usage and max history usage is always 1.
    The usage of PortInDirect is the same as PortIn. They just have different
    behaviours for saving the value.
    """

    def __init__(self) -> 'T':
        # Create the direct input buffer.
        self.__buffer = None

    def current(self) -> 'T':
        """ This method performs the same as the `get()` method for
        PortInDirect.
        Returns: The last hold value.
        """
        return self.__buffer

    def size(self) -> int:
        """ The size of a PortInDirect is constant 1. Only 1 value will be hold
        for the port.
        Returns: The constant value 1.
        """
        return 1

    def max_size(self) -> int:
        """ The maximum size of a PortInDirect is constant 1. Only 1 value will
        be hold for the port.
        Returns: The constant value 1.
        """
        return 1

    def set_max_size(self, max_size: int) -> None:
        """ Override for avoid setting the maximum holding size. It is
        constantly 1 for PortInDirect.
        """
        pass

    def set_almost_full_size(self, almost_full: int = -1) -> None:
        """ Override for avoid setting the maximum holding size. It is
        constantly 1 for PortInDirect.
        """
        pass

    def is_full(self) -> bool:
        """ A constant `False` will be returned for the port.
        Returns: Constant `False` for full checking.
        """
        return False

    def is_empty(self) -> bool:
        """ A constant `False` will be returned for the port.
        Returns: Constant `False` for empty checking.
        """
        return False

    def head(self) -> 'T':
        """ Get the first coming item in the queue.
        Returns: If the queue is not empty, it will show the first item
        contained in the port, or else None.
        """
        return self.__buffer

    def put(self, value: 'T') -> None:
        """ Put a value to the direct in port.
        Args:
            value: The new value of the port to hold.
        """
        # Simply save the value to buffer, no task will be returned.
        self.__buffer = value

    def get(self) -> 'T':
        """ Get the last value hold by the port. The default value is None.
        Returns: The last hold value.
        """
        # Directly get the value from buffer
        return self.__buffer

    def max_history_usage(self) -> int:
        """ The maximum history usage of the PortInDirect is constant 1.
        Returns: The constant history usage value, 1.
        """
        # Constant usage.
        return 1

    def current_usage(self) -> int:
        """ The current usage of the port is constant 1.
        Returns: The constant usage, 1.
        """
        # Constant usage.
        return 1


class PortOut:
    """ PortOut class describes an output port, which allows multiple input
    ports to connect to it. It manages a list of input ports and send the data
    to those ports in the order of appending.
    To connect a PortIn type to the PortOut type, use `connect()` method to
    attach a PortIn type port to the PortOut type.
    There will be no usage statistics for the PortOut, because it is only used
    for sending messages to all the PortIn.
    However, it does have a full state checks. If any one of the ports connected
    to the PortOut is marked as full, the PortOut port will be marked as full.
    Examples:
        To create a PortOut manually, for example, a PortOut type named `enable`
        inside a Node::
            self.port_out(enable)
        Connect the PortIn type variable `write_enable` from `ram` to the
        PortOut type variable `enable` of the current Node::
            self.enable.connect(ram.write_enable)
        To put or send a value to all the ports connect to a PortOut type, for
        example, sending a 'True' value to all the connected ports to the
        `enable` port of the current Node::
            self.enable.put(True)
    """
    def __init__(self) -> 'T':
        """ Initialize a PortOut class.
        This method is usually not called by user, but by PyHLM internally. It
        is not suggested to modify the PortIn class unless you understand what
        you are doing.
        """
        # Contains a list of connected input ports.
        self.__port_in = None
        # A buffer for holding the current value.
        self.__current = None

    def current(self) -> 'T':
        """ Get the last posted value to all the connected PortIn variables.
        Returns: The last posted value cache. If no value has been posted
        before, return `None`.
        """
        return self.__current

    def connect(self, port: PortIn) -> None:
        """ Connect a input port to the current output port.
        Args:
            port: The PortIn type input port.
        """
        # Add one input target to the current output port.
        if self.__port_in is not None:
            raise Exception('Port {} is already connected to Port {}, '
                            'one PortOut could only connect to one '
                            'PortIn.'.format(str(self), str(self.__port_in)))
        self.__port_in = port

    def put(self, value: 'T') -> None:
        """ Put a value to all the connected ports.
        Args:
            value: The value to be put to all the target.
        """
        # Save the value to the buffer.
        self.__current = value
        # Broadcast the value to all the input port in the list.
        if not self.is_no_connection():
            self.__port_in.put(self.__current)

    def is_no_connection(self) -> bool:
        """ To check whether there is no ports connect to the current PortOut.
        Returns: If no ports are connected to the PortOut before, return True,
        otherwise, False.
        """
        # Check whether this is an empty port.
        return self.__port_in is None

    def is_full(self) -> bool:
        """ If any one of the connected PortIn is full, the PortOut is treated
        as full. This method checks whether the port is full or not.
        Returns: If one of the connected PortIn is full, return True, otherwise
        False.
        """
        # Loop and check all the port in the list.
        return False if self.is_no_connection() else self.__port_in.is_full()

    def connected_port(self) -> PortIn:
        """ Get the connected port of the current port.
        Returns: The connected port object.
        """
        return self.__port_in